package io.simple.core;

import injections.annotations.PostInject;
import injections.annotations.Property;
import io.simple.core.chain.Filter;
import io.simple.core.chain.Pipeline;
import io.simple.core.chain.RequestHandler;
import io.simple.core.handlers.MultiHandler;
import io.simple.core.handlers.MultiHandlerHelper;
import io.simple.monitoring.FilterMonitor;
import io.simple.monitoring.HandlerMonitor;
import io.simple.monitoring.NullMonitor;
import io.simple.signals.Signal;
import io.simple.signals.Signals;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import simple.utils.processing.Timer;

import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Process request through chain of input/output filters and appropriate handlers
 * <p>
 * User: rehak
 * Date: 8/20/13
 * Time: 2:50 PM
 * To change this template use File | Settings | File Templates.
 */
public class DefaultPipeline implements Pipeline {

    private static final Logger LOGGER = LoggerFactory.getLogger(DefaultPipeline.class);
    @Property(optional = true)
    private HandlerMonitor hmonitor = new NullMonitor();
    @Property(optional = true)
    private FilterMonitor fmonitor = new NullMonitor();
    @Property(optional = true)
    private final List<RequestHandler> handlers = new CopyOnWriteArrayList<>();
    @Property(optional = true)
    private final List<MultiHandler> multiHandlers = new CopyOnWriteArrayList<>();
    @Property(optional = true)
    private final List<Filter> inFilters = new CopyOnWriteArrayList<>();
    @Property(optional = true)
    private final List<Filter> outFilters = new CopyOnWriteArrayList<>();
    private Map<Pattern, RequestHandler> handlerMap = new ConcurrentHashMap<>();

    public DefaultPipeline() {

    }

    public DefaultPipeline(Collection<RequestHandler> handlers, List<Filter> inFilters, List<Filter> outFilters, List<MultiHandler> multiHandlers) {
        if (handlers != null) {
            this.handlers.addAll(handlers);
        }
        if (multiHandlers != null) {
            this.multiHandlers.addAll(multiHandlers);
        }
        if (inFilters != null) {
            this.inFilters.addAll(inFilters);
        }
        if (outFilters != null) {
            this.outFilters.addAll(outFilters);
        }
        createBindings();
    }

    @PostInject
    private void postInject() {
        createBindings();
    }

    public void createBindings() {
        // process MultiHandlers
        for (MultiHandler mh : multiHandlers) {
            List<RequestHandler> created = MultiHandlerHelper.createHandlers(mh);
            this.handlers.addAll(created);
        }
        // create pattern map
        for (RequestHandler handler : handlers) {
            handlerMap.put(handler.getPath(), handler);
        }
    }

    @Override
    public boolean canProcess(String path) {
        if (getHandler(path) != null) {
            return true;
        }
        return false;
    }

    private RequestHandler getHandler(String path) {
        for (Pattern p : handlerMap.keySet()) {
            if (p.matcher(path).matches()) {
                return handlerMap.get(p);
            }
        }
        return null;
    }

    @Override
    public ByteBuffer process(RequestContext ctx, ByteBuffer data) throws Signal {
        // path
        RequestHandler handler = null;
        for (Pattern p : handlerMap.keySet()) {
            Matcher matcher = p.matcher(ctx.getRequestPath());
            if (matcher.matches()) {
                handler = handlerMap.get(p);
                ctx.getPathGroups().put(0, matcher.group());
                for (int i = 1; i < matcher.groupCount() + 1; i++) {
                    String grp = matcher.group(i);
                    if (grp != null) {
                        ctx.getPathGroups().put(i, grp);
                    } else {
                        //LOGGER.warn("Group " + i + " is null");
                        // group is null - non capturing probably
                    }
                }
                break;
            }
        }
        // handler not found?
        if (handler == null) {
            hmonitor.notFound(ctx.getRequestPath());
            Signals.throwStat(Signals.BAD_REQUEST, "Handler not found for " + ctx.getRequestPath());
        }
        // apply filters
        data = processFilters(inFilters, ctx, data, handler.getPath());
        // process with handler
        final Timer t = new Timer();
        try {
            hmonitor.invoke(handler.getPath());
            t.start();
            // PROCESSING
            data = handler.processRequest(ctx, data);
            t.stop();
            hmonitor.time(handler.getPath(), t.getDuration(TimeUnit.MILLISECONDS));
        } catch (Signal s) {
            hmonitor.signal(handler.getPath(), s);
            throw s;
        } catch (Exception e) {
            hmonitor.exception(handler.getPath(), e);
            throw Signals.newStat(Signals.INTERNAL_SERVER_ERROR, "Error processing handler for " + ctx.getRequestPath(), e);
        }
        // apply outgoing filters
        data = processFilters(outFilters, ctx, data, handler.getPath());
        return data;
    }

    private ByteBuffer processFilters(Collection<Filter> filters, RequestContext rc, ByteBuffer data, Pattern path) throws Signal {
        if (filters != null) {
            for (Filter f : filters) {
                try {
                    Timer t = new Timer();
                    fmonitor.invoke(path, f.getClass().getName());
                    t.start();
                    data = f.filter(rc, data);
                    t.stop();
                    fmonitor.time(path, t.getDuration(TimeUnit.MILLISECONDS), f.getClass().getName());
                    data.rewind();
                } catch (Signal s) {
                    fmonitor.signal(path, s, f.getClass().getName());
                    throw s;
                } catch (Exception e) {
                    fmonitor.exception(path, e, f.getClass().getName());
                    throw Signals.newStat(Signals.INTERNAL_SERVER_ERROR, "Error processing filters!", e);
                }
            }
        }
        return data;
    }


}
